/*
 * Copyright 2013 Solace Systems, Inc.
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.solacesystems.tools.ant;

import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.message.BasicNameValuePair;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * SAX Event handler that extracts URIs from html documents
 * @author swilson
 *
 */
public class LinkFinder extends DefaultHandler {

	private static final String[] DEFAULT_LINK_ATTRIBUTES = {"href", "src", "action"};
	
	/**
	 * Interested parties should subclass this to receive events from a LinkFinder
	 * @author swilson
	 *
	 */
	public static abstract class LinkEventListener {
		/**
		 * Called by a LinkFinder when a link is found
		 * @param location The location within a document
		 * @param link The URI that was discovered
		 */
		public abstract void linkFound(Location location, URI link);
		
		/**
		 * Called by a LinkFinder when an exception is throw while parsing a document
		 * @param location Location within the document
		 * @param e The exception that was thrown
		 */
		public abstract void error(Location location, Exception e);
	}
	
	private List<LinkEventListener> m_Listeners = new ArrayList<LinkEventListener>();
	
	private Locator m_CurrentLocator;
	private URI m_CurrentURI;
	private URI m_BaseURI;
	private URISyntaxException m_URISyntaxException;

	private String[] m_LinkAttrNames = DEFAULT_LINK_ATTRIBUTES.clone();
	
	/**
	 * List of HTML attributes that are parsed as URIs
	 * @param names
	 */
	public void setLinkAttributeNames(String[] names) {
		m_LinkAttrNames = names;
	}
	
	/**
	 * List of HTML attributes that are parsed as URIs
	 * @return Array of attribute names
	 */
	public String[] getLinkAttributeNames() {
		return m_LinkAttrNames;
	}
	
	@Override
	public void startDocument() throws SAXException {
		m_BaseURI = null;
	}
	
	@Override
	public void setDocumentLocator(Locator locator) {
		m_CurrentLocator = locator;
		try {
			m_CurrentURI = new URI(locator.getSystemId()).normalize();
		} catch (URISyntaxException e) {
			m_CurrentURI = null;
			m_URISyntaxException = e;
		}
	}
	
	public Locator getCurrentLocator() {
		return m_CurrentLocator;
	}
	
	/**
	 * Return the URI of the document the LinkFinder is parsing
	 * @return URI of the current document
	 * @throws URISyntaxException
	 */
	public URI getDocumentURI() throws URISyntaxException {
		if (m_URISyntaxException != null) {
			URISyntaxException e = m_URISyntaxException;
			m_URISyntaxException = null;
			throw e;
		}
		
		return m_CurrentURI;
	}
	
	/**
	 * Convert a relative address in String format to an absolute URI
	 * @param addr Address to make absolute
	 * @return An absolute version of the address as a URI
	 * @throws URISyntaxException Thrown when the address cannot be parsed as a URI
	 */
	protected URI relativeToAbsolute(String addr) throws URISyntaxException {
		URI base = m_BaseURI == null ? getDocumentURI() : m_BaseURI;
	
		// Add a trailing slash if none exists, but only for http URIs
		if (base.getScheme().toLowerCase().startsWith("http")) {
			if (base.getPath() == null || !base.getPath().endsWith("/")) {
				base = new URI(base.toString() + "/");
			}
		}
		
		URIBuilder builder = new URIBuilder(addr);
		URI uri = builder.build();
		return base.resolve(uri).normalize();
	}

	/**
	 * Attempts to work around common quirks in URIs found in HTML documents
	 * @param link
	 * @return A probably more valid, but hopefully still equivalent, address
	 */
	protected String fixLink(String link) {
		String query = "";
		
		int f_idx = link.indexOf('#');
		if (f_idx >= 0) {
			link = link.substring(0, f_idx);
		}
		
		int q_idx = link.indexOf('?');
		if (q_idx >= 0) {
			query = link.substring(q_idx+1);
			link = link.substring(0, q_idx);
			List<NameValuePair> vars = URLEncodedUtils.parse(query, Charset.defaultCharset());
			List<NameValuePair> fixed = new ArrayList<NameValuePair>();
			for (NameValuePair p : vars) {
				fixed.add(
						new BasicNameValuePair(
								p.getName(),
								p.getValue()));
			}
			query = '?' + URLEncodedUtils.format(fixed, Charset.defaultCharset());
		}
		
		
		return (link + query).trim();
	}
	
	protected void addLink(String link) throws URISyntaxException {
		link = fixLink(link);
		URI absolute = relativeToAbsolute(link);
		triggerLinkFound(m_CurrentLocator, absolute);
	}
	
	protected void addLinksFrom(Attributes attrs) {
		for (String name : m_LinkAttrNames) {
			String value = attrs.getValue(name);
			
			if (value != null) {
				try {
					addLink(value);
				} catch (URISyntaxException e) {
					triggerError(m_CurrentLocator, e);
				}
			}
		}
	}
	
	@Override
	public void startElement(String uri, String localName, String qName,
			Attributes attributes) throws SAXException {
		if (localName.equalsIgnoreCase("base")) {
			if (m_BaseURI == null) {	// Ignore multiple base tags as per MDN
				try {
					String href = attributes.getValue("href");
					if (href != null) {
						m_BaseURI = relativeToAbsolute(href);
					}
				} catch (URISyntaxException e) {
					triggerError(m_CurrentLocator, e);
				}
			}
		} else {
			addLinksFrom(attributes);
		}
	}

	public void reset() {
		m_BaseURI = null;
		m_CurrentLocator = null;
		m_CurrentURI = null;
		m_URISyntaxException = null;
	}
	
	protected void triggerLinkFound(Locator location, URI link) {
		synchronized (m_Listeners) {
			Location l = Location.fromLocator(location);
			for (LinkEventListener listener : m_Listeners) {
				listener.linkFound(l, link);
			}
		}
	}
	
	protected void triggerError(Locator location, Exception e) {
		synchronized (m_Listeners) {
			Location l = Location.fromLocator(location);
			for (LinkEventListener listener : m_Listeners) {
				listener.error(l, e);
			}
		}
	}
	
	/**
	 * Interested parties can attach an instance of LinkEventListener to
	 * receive events from the LinkFinder
	 * @param listener
	 */
	public void addLinkEventListener(LinkEventListener listener) {
		synchronized (m_Listeners) {
			m_Listeners.add(listener);
		}
	}
	
	/**
	 * Interested parties can attach an instance of LinkEventListener to
	 * receive events from the LinkFinder
	 * @param listener
	 */
	public void removeLinkEventListener(LinkEventListener listener) {
		synchronized (m_Listeners) {
			m_Listeners.remove(listener);
		}
	}
}
